#ifndef PORTABLE_LITERALS_H_
#define PORTABLE_LITERALS_H_

#include <vector>

#include "src/pbc/portable-format-helper.h"
#include "src/pbc/portable-globals.h"
#include "src/pbc/portable-primitives.h"

namespace portable {

class ClassDescription;
// todo(hh): support isolate->factory()->uninitialized_value()?
class LiteralElement {
 public:
  enum class Tag : uint8_t {
    kNull,
    kUndefined,
    kUninitialized,
    kTheHole,
    kTrue,
    kFalse,
    kSmi,
    kNumber,
    kString,
    kBigInt,
    kObject,
    kArray,
    kClass,
  };

  class ObjectDescript {
   public:
    ObjectDescript(uint32_t boilerplate_description_flag,
                   uint32_t boilerplate_properties, uint32_t property_length,
                   uint32_t index_keys, bool has_seen_proto)
        : boilerplate_description_flag_(boilerplate_description_flag),
          boilerplate_properties_(boilerplate_properties),
          property_length_(property_length),
          index_keys_(index_keys),
          has_seen_proto_(has_seen_proto) {}
    ObjectDescript() = default;

    bool IsDefaultObjectDescript(uint32_t number_of_properties) {
      return boilerplate_description_flag_ == ObjectLiteral::kFastElements &&
             boilerplate_properties_ == number_of_properties &&
             property_length_ == number_of_properties && index_keys_ == 0 &&
             has_seen_proto_ == false;
    }

    bool operator==(const ObjectDescript& rhs) const {
      return boilerplate_description_flag_ ==
                 rhs.boilerplate_description_flag_ &&
             boilerplate_properties_ == rhs.boilerplate_properties_ &&
             property_length_ == rhs.property_length_ &&
             index_keys_ == rhs.index_keys_ &&
             has_seen_proto_ == rhs.has_seen_proto_;
    }

    uint32_t boilerplate_description_flag_;
    uint32_t boilerplate_properties_;
    uint32_t property_length_;
    uint32_t index_keys_;
    bool has_seen_proto_;
  };

  static LiteralElement CreateNull() { return LiteralElement(Tag::kNull); }

  static LiteralElement CreateUndefined() {
    return LiteralElement(Tag::kUndefined);
  }

  static LiteralElement CreateUninitialized() {
    return LiteralElement(Tag::kUninitialized);
  }

  static LiteralElement CreateTheHole() {
    return LiteralElement(Tag::kTheHole);
  }

  static LiteralElement CreateTrue() { return LiteralElement(Tag::kTrue); }

  static LiteralElement CreateFalse() { return LiteralElement(Tag::kFalse); }

  static LiteralElement CreateSmi(Smi val) { return LiteralElement(val); }

  static LiteralElement CreateNumber(double num) { return LiteralElement(num); }

  static LiteralElement CreateString(String str) {
    return LiteralElement(Tag::kString, str);
  }

  static LiteralElement CreateObject(ObjectDescript desc,
                                     std::vector<LiteralElement> elements) {
    return LiteralElement(Tag::kObject, desc, elements);
  }

  static LiteralElement* NewObject(ObjectDescript desc,
                                   std::vector<LiteralElement> elements) {
    return new LiteralElement(Tag::kObject, desc, elements);
  }

  static LiteralElement CreateArray(bool is_cow, int8_t kind,
                                    std::vector<LiteralElement> elements) {
    return LiteralElement(Tag::kArray, is_cow, kind, elements);
  }

  static LiteralElement* NewArray(bool is_cow, int8_t kind,
                                  std::vector<LiteralElement> elements) {
    return new LiteralElement(Tag::kArray, is_cow, kind, elements);
  }

  static LiteralElement CreateClass(ClassDescription* desc) {
    return LiteralElement(Tag::kClass, desc);
  }

  static LiteralElement* NewClass(ClassDescription* desc) {
    return new LiteralElement(Tag::kClass, desc);
  }

  // expect no tailing 'n'
  static LiteralElement CreateBigInt(const std::string& bigint) {
    P_CHECK(bigint.back() != 'n');
    return LiteralElement(Tag::kBigInt, bigint);
  }

  ~LiteralElement();

  String string() const {
    P_DCHECK(tag_ == Tag::kString);
    return string_;
  }

  const std::string& bigint() const {
    P_DCHECK(tag_ == Tag::kBigInt);
    P_DCHECK(std::holds_alternative<std::string>(string_));
    return std::get<std::string>(string_);
  }

  Smi smi() const {
    P_DCHECK(tag_ == Tag::kSmi);
    return smi_;
  }

  double number() const {
    P_DCHECK(tag_ == Tag::kNumber);
    return number_;
  }

  int null() const {
    P_DCHECK(tag_ == Tag::kNull);
    return constant_;
  }

  int true_val() const {
    P_DCHECK(tag_ == Tag::kTrue);
    return constant_;
  }

  int false_val() {
    P_DCHECK(tag_ == Tag::kFalse);
    return constant_;
  }

  const ObjectDescript get_obj_desc() const { return obj_desc_; }

  const ClassDescription* get_class_desc() const { return class_desc_; }

  bool is_cow() const { return is_cow_; }

  uint8_t kind() const { return kind_; }

  const std::vector<LiteralElement>& get_elements() const { return elements_; }

  Tag get_tag() const { return tag_; }

  bool operator==(const LiteralElement& rhs) const {
    if (tag_ != rhs.tag_) {
      return false;
    }

    switch (tag_) {
      case Tag::kNull:
      case Tag::kUndefined:
      case Tag::kUninitialized:
      case Tag::kTheHole:
      case Tag::kTrue:
      case Tag::kFalse:
        return true;

      case Tag::kSmi:
        return Smi::SmiEqual()(smi_, rhs.smi_);

      case Tag::kNumber:
        if (number_ == rhs.number_) {
          if (number_ == 0.0 && rhs.number_ == 0.0) {
            if (std::signbit(number_) != std::signbit(rhs.number_)) {
              return false;
            }
          }
          return true;
        } else {
          return false;
        }

      case Tag::kString:
      case Tag::kBigInt:
        return string_ == rhs.string_;

      case Tag::kArray:
      case Tag::kObject: {
        if (elements_.size() != rhs.elements_.size()) {
          return false;
        }
        for (uint32_t i = 0; i < elements_.size(); ++i) {
          if (elements_[i] != rhs.elements_[i]) {
            return false;
          }
        }
        if (tag_ == Tag::kArray) {
          return is_cow_ == rhs.is_cow_ && kind_ == rhs.kind_;
        } else if (tag_ == Tag::kObject) {
          return get_obj_desc() == rhs.get_obj_desc();
        }
        return true;
      }
      default:
        return false;
    }
  }

 private:
  explicit LiteralElement(Tag tag) : tag_(tag) {}
  explicit LiteralElement(Smi smi) : tag_(Tag::kSmi), smi_(smi) {}
  explicit LiteralElement(double num) : tag_(Tag::kNumber), number_(num) {}
  explicit LiteralElement(Tag tag, String str) : tag_(tag), string_(str) {
    P_DCHECK(tag_ == Tag::kString || tag_ == Tag::kBigInt);
  }
  explicit LiteralElement(Tag tag, ObjectDescript desc,
                          std::vector<LiteralElement> elements)
      : tag_(tag), obj_desc_(desc), elements_(elements) {
    P_DCHECK(tag_ == Tag::kObject);
  }
  explicit LiteralElement(Tag tag, bool is_cow, uint8_t kind,
                          std::vector<LiteralElement> elements)
      : tag_(tag), is_cow_(is_cow), kind_(kind), elements_(elements) {
    P_DCHECK(tag_ == Tag::kArray);
  }
  explicit LiteralElement(Tag tag, ClassDescription* desc)
      : tag_(tag), class_desc_(desc) {
    P_DCHECK(tag_ == Tag::kClass);
  }
  const Tag tag_;
  // todo(hh): maybe use union here?
  // For both string and bigint, bigint expect no tailing 'n'
  String string_;
  Smi smi_ = Smi::zero();
  double number_ = 0;
  int constant_ = 0;
  ObjectDescript obj_desc_;
  bool is_cow_;
  uint8_t kind_;
  std::vector<LiteralElement> elements_;
  ClassDescription* class_desc_;
};

class ClassDescription final {
 public:
  struct PropertyDescription {
    bool is_static_;
    bool is_private_;
    bool is_computed_name_;
    enum Kind : uint8_t { METHOD, GETTER, SETTER, FIELD, AUTO_ACCESSOR };
    Kind kind_;
    LiteralElement key_;
  };
  ClassDescription() {}
  ClassDescription(int start, int end,
                   const std::vector<PropertyDescription>& prop)
      : start_(start), end_(end), prop_(std::move(prop)) {}

  int start_ = 0;
  int end_ = 0;
  std::vector<PropertyDescription> prop_;
};

class TemplateObjectDescription final {
 public:
  TemplateObjectDescription(bool dup_) : dup(dup_) {}
  void AddRaw(LiteralElement ele) { raws.push_back(ele); }
  void AddCooked(LiteralElement ele) { cookeds.push_back(ele); }
  const std::vector<LiteralElement>& GetRaws() const { return raws; }
  const std::vector<LiteralElement>& GetCookeds() const { return cookeds; }
  bool IsDup() const { return dup; }

 private:
  bool dup;
  std::vector<LiteralElement> raws;
  std::vector<LiteralElement> cookeds;
};

}  // namespace portable

#endif  // PORTABLE_LITERALS_H_